package Hunters;

import cz.cuni.amis.pogamut.base.agent.navigation.IPathExecutorState;
import cz.cuni.amis.pogamut.base.utils.guice.AgentScoped;
import cz.cuni.amis.pogamut.ut2004.agent.module.sensor.NavPoints;
import cz.cuni.amis.pogamut.ut2004.agent.module.utils.TabooSet;
import cz.cuni.amis.pogamut.ut2004.agent.navigation.IUT2004Navigation;
import cz.cuni.amis.pogamut.ut2004.agent.navigation.UT2004Navigation;
import cz.cuni.amis.pogamut.ut2004.agent.navigation.UT2004PathAutoFixer;
import cz.cuni.amis.pogamut.ut2004.agent.navigation.UT2004PathExecutorStuckState;
import cz.cuni.amis.pogamut.ut2004.bot.impl.UT2004BotModuleController;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbcommands.Initialize;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.*;
import cz.cuni.amis.pogamut.ut2004.utils.UT2004BotRunner;
import cz.cuni.amis.utils.collections.MyCollections;
import cz.cuni.amis.utils.exception.PogamutException;
import cz.cuni.amis.utils.flag.FlagListener;

import java.util.logging.Level;

@AgentScoped
public class NavigationBot extends UT2004BotModuleController {
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    public boolean shouldSpeak = true;
    public boolean shouldLog = false;

    private void say(String text) {
        if (shouldSpeak) {
            body.getCommunication().sendGlobalTextMessage(text);
        }
        if (shouldLog) {
            say(text);
        }
    }

    /////////////////////////////////////////////
    public boolean useAStar = false;
    public boolean useNavMesh = false;
    public boolean drawNavMesh = true;

    private void chooseNavigationToUse() {
        if (useAStar) {
            if (navigationToUse != navigationAStar) {
                say("Using UT2004AStar to find path.");
                if (navigationToUse != null)
                    navigationToUse.stopNavigation();
                navigationToUse = navigationAStar;
                info.getBotName().setInfo("UT2004-ASTAR");
            }
        } else if (useNavMesh) {
            if (nmNav.isAvailable()) {
                if (navigationToUse != nmNav) {
                    say("Using NavMesh for navigation.");
                    if (navigationToUse != null)
                        navigationToUse.stopNavigation();
                    navigationToUse = nmNav;
                    info.getBotName().setInfo("NAVMESH");
                }
            } else {
                log.warning("NavMesh not available! See startup log for more details.");
            }
        }
        if (navigationToUse == null || (!useAStar && !useNavMesh)) {
            if (navigationToUse != navigation) {
                say("Using FloydWarshallMap to find path.");
                if (navigationToUse != null)
                    navigationToUse.stopNavigation();
                navigationToUse = navigation;
                info.getBotName().setInfo("FW");
            }
        }
    }

    ////////////////////////////////////////////////////
    private boolean navMeshDrawn = false;
    private int waitForMesh;
    private double waitingForMesh;

    private boolean drawNavMesh() {
        if (!navMeshDrawn) {
            navMeshDrawn = true;
            say("Drawing NavMesh...");
            navMeshModule.getNavMeshDraw().clearAll();
            navMeshModule.getNavMeshDraw().draw(true, false);
            say("Okey, drawing commands issued, now we have to wait a bit till it gets drawn completely...");
            waitForMesh = navMeshModule.getNavMesh().getPolys().size() / 35;
            waitingForMesh = -info.getTimeDelta();
        }
        if (waitForMesh > 0) {
            waitForMesh -= info.getTimeDelta();
            waitingForMesh += info.getTimeDelta();
            if (waitingForMesh > 2) {
                waitingForMesh = 0;
                say(((int) Math.round(waitForMesh)) + "s...");
            }
            if (waitForMesh > 0) {
                return false;
            }
        }
        return true;
    }

    //////////////////////////////////////////////////
    private boolean offMeshLinksDrawn = false;
    private int waitForOffMeshLinks;
    private double waitingForOffMeshLinks;

    private boolean drawOffMeshLinks() {
        if (!offMeshLinksDrawn) {
            offMeshLinksDrawn = true;
            if (navMeshModule.getNavMesh().getOffMeshPoints().size() == 0) {
                say("Ha! There are no off-mesh points / links within this map!");
                return true;
            }
            say("Drawing OffMesh Links...");
            navMeshModule.getNavMeshDraw().draw(false, true);
            say("Okey, drawing commands issued, now we have to wait a bit till it gets drawn completely...");
            waitForOffMeshLinks = navMeshModule.getNavMesh().getOffMeshPoints().size() / 10;
            waitingForOffMeshLinks = -info.getTimeDelta();
        }

        if (waitForOffMeshLinks > 0) {
            waitForOffMeshLinks -= info.getTimeDelta();
            waitingForOffMeshLinks += info.getTimeDelta();
            if (waitingForOffMeshLinks > 2) {
                waitingForOffMeshLinks = 0;
                say(((int) Math.round(waitForOffMeshLinks)) + "s...");
            }
            if (waitForOffMeshLinks > 0) {
                return false;
            }
        }

        return true;
    }

    //////////////////////////////////////////////
    private void logNavigation() {
        if (navigationToUse.getCurrentTargetPlayer() != null) {
            say("-> " + NavPoints.describe(navigationToUse.getCurrentTargetPlayer()));
        } else {
            say("-> " + NavPoints.describe(navigationToUse.getCurrentTarget()));
        }
        int pathLeftSize = navigationToUse.getPathExecutor().getPath() == null ? 0 : navigationToUse.getPathExecutor().getPath().size() - navigationToUse.getPathExecutor().getPathElementIndex();
        say("Path points left:   " + pathLeftSize);
        say("Remaining distance: " + navigationToUse.getRemainingDistance());
        say("Visible navpoints:  " + world.getAllVisible(NavPoint.class).size() + " / " + world.getAll(NavPoint.class).size());
        say("Visible items:      " + items.getVisibleItems().values() + " / " + world.getAll(Item.class).size());
        say("Visible players:    " + players.getVisiblePlayers().size());
    }

    ////////////////////////////////////////////
    protected int talking;

    private void talkTo(Player player) {
        move.turnTo(player);
        switch (talking) {
            case 0:
                say("Hi!");
                break;
            case 4:
                say("Howdy!");
                break;
            case 10:
                say("I'm NavigationBot made to fool around and test Pogamut's navigation stuff!");
                break;
            case 18:
                say("My work is extremely important.");
                break;
            case 24:
                say("So do not interrupt me, ok?");
                break;
            case 26:
                if (random.nextDouble() > 0.5) {
                    move.jump();
                }
                break;
            case 40:
                say(getRandomLogoutMessage());
                break;
        }
        ++talking;
        if (talking > 40) {
            talking = 25;
        }
    }

    //////////////////////////////////////////
    private String getRandomLogoutMessage() {
        switch (random.nextInt(8)) {
            case 0:
                return "I would appriciate if you log out or switch to SPECTATE mode.";
            case 1:
                return "Would you please log out or switch to SPECTATE mode.";
            case 2:
                return "Just log out, will ya?";
            case 3:
                return "As I've said, I'M SOMEONE, so log out, ok?";
            case 4:
                return "I can see you don't get it... LOGOUT! OK!";
            case 5:
                return "I hate when humans are so clueless... just press ESCAPE key and press Spectate button, that's all I want!";
            case 6:
                return "I guess you do not know how to switch to spectate mode, right? Just press ESCAPE key and press Spectate button";
            default:
                return "AHAHAHAYYYAAAA!";
        }

    }

    //////////////////////////////////////////////
    private void handlePlayerNavigation() {
        if (navigationToUse.isNavigating() && navigationToUse.getCurrentTargetPlayer() != null) {
            logNavigation();
            return;
        }

        Player player = players.getNearestVisiblePlayer();
        if (player == null) {
            handleNavPointNavigation();
            return;
        }

        if (info.getLocation().getDistance(player.getLocation()) < UT2004Navigation.AT_PLAYER) {
            talkTo(player);
            return;
        }

        navigationToUse.navigate(player);
        logNavigation();
    }

    //////////////////////////////////////////
    protected NavPoint getRandomNavPoint() {
        say("Picking new target navpoint.");
        NavPoint chosen = MyCollections.getRandomFiltered(getWorldView().getAll(NavPoint.class).values(), tabooNavPoints);
        if (chosen != null) {
            return chosen;
        }
        log.warning("All navpoints are tabooized at this moment, choosing navpoint randomly!");
        return MyCollections.getRandom(getWorldView().getAll(NavPoint.class).values());
    }

    /////////////////////////////////////////////
    private void handleNavPointNavigation() {
        if (navigationToUse.isNavigatingToNavPoint()) {
            while (navigationToUse.getContinueTo() == null && navigationToUse.getRemainingDistance() < 400) {
                NavPoint nextNavPoint = getRandomNavPoint();
                say("EXTENDING THE PATH: " + NavPoints.describe(nextNavPoint));
                navigationToUse.setContinueTo(nextNavPoint);
            }
            logNavigation();
            return;
        }
        targetNavPoint = getRandomNavPoint();
        if (targetNavPoint == null) {
            log.severe("COULD NOT CHOOSE ANY NAVIGATION POINT TO RUN TO!!!");
            if (world.getAll(NavPoint.class).size() == 0) {
                log.severe("world.getAll(NavPoint.class).size() == 0, there are no navigation ponits to choose from! Is exporting of nav points enabled in GameBots2004.ini inside UT2004?");
            }
            config.setName("NavigationBot [CRASHED]");
            return;
        }

        talking = 0;

        say("CHOOSING FIRST NAVPOINT TO RUN TO: " + NavPoints.describe(targetNavPoint));
        navigationToUse.navigate(targetNavPoint);
        logNavigation();
    }

    //////////////////////////////////////////////


/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    @Override
    public void beforeFirstLogic() {
    }

    /////////////////////////////////////////////////////
    @Override
    public void logic() {
        say("--- Logic iteration ---");
        chooseNavigationToUse();//选择一种导航用，nmNav

        if (navigationToUse == nmNav && drawNavMesh) {
            if (!drawNavMesh())
                return;
            if (!drawOffMeshLinks())
                return;
        }

        if (players.canSeePlayers() || navigationToUse.getCurrentTargetPlayer() != null) {
            handlePlayerNavigation();
        } else {
            handleNavPointNavigation();
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    @Override
    public void botKilled(BotKilled event) {
        navigation.stopNavigation();
    }

    //////////////////////////////////////////////////////////////////////////////////////
    @Override
    public void mapInfoObtained() {
        navMeshModule.setReloadNavMesh(true);
    }


    /////////////////////////////////////////////////////////////////////
    protected void pathExecutorStateChange(IPathExecutorState event) {
        switch (event.getState()) {
            case PATH_COMPUTATION_FAILED:
                tabooNavPoints.add(targetNavPoint, 180);
                break;

            case TARGET_REACHED:
                tabooNavPoints.add(targetNavPoint, 180);
                break;

            case STUCK:
                UT2004PathExecutorStuckState stuck = (UT2004PathExecutorStuckState) event;
                if (stuck.isGlobalTimeout()) {
                    say("UT2004PathExecutor GLOBAL TIMEOUT!");
                } else {
                    say(stuck.getStuckDetector() + " reported STUCK!");
                }
                if (stuck.getLink() == null) {
                    say("STUCK LINK is NOT AVAILABLE!");
                } else {
                    say("Bot has stuck while running from " + stuck.getLink().getFromNavPoint().getId() + " -> " + stuck.getLink().getToNavPoint().getId());
                }
                tabooNavPoints.add(targetNavPoint, 60);
                break;
            case STOPPED:
                targetNavPoint = null;
                break;
        }
    }

    ////////////////////////////////////////////////////////////////////////////////////
    public FlagListener<IPathExecutorState> FlagListener = new FlagListener<IPathExecutorState>() {
        @Override
        public void flagChanged(IPathExecutorState changedValue) {
            pathExecutorStateChange(changedValue);
        }
    };

    /////////////////////////////////////////////////////////////////////////
    FlagListener<IPathExecutorState> flaglistener = new FlagListener<IPathExecutorState>() {
        @Override
        public void flagChanged(IPathExecutorState changedValue) {
            pathExecutorStateChange(changedValue);
        }
    };

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    protected TabooSet<NavPoint> tabooNavPoints;
    protected NavPoint targetNavPoint;
    protected UT2004PathAutoFixer autoFixer;
    protected UT2004Navigation navigationAStar;
    public Level navigationLogLevel = Level.WARNING;
    protected IUT2004Navigation navigationToUse;


    @Override
    public void botInitialized(GameInfo gameInfo, ConfigChange config, InitedMessage init) {
        tabooNavPoints = new TabooSet<NavPoint>(bot);
        autoFixer = new UT2004PathAutoFixer(bot, navigation.getPathExecutor(), fwMap, aStar, navBuilder);

        navigation.getPathExecutor().getState().addStrongListener(FlagListener);
        nmNav.getPathExecutor().getState().addStrongListener(flaglistener);

        navigationAStar = new UT2004Navigation(bot, navigation.getPathExecutor(), aStar, navigation.getBackToNavGraph(), navigation.getRunStraight());
        navigationAStar.getLog().setLevel(navigationLogLevel);

        navigation.getLog().setLevel(navigationLogLevel);

        nmNav.setLogLevel(navigationLogLevel);
    }

    ////////////////////////////////
    @Override
    public void botFirstSpawn(GameInfo gameInfo, ConfigChange config, InitedMessage init, Self self) {
        navigation.getPathExecutor().getLog().setLevel(Level.ALL);
        nmNav.setLogLevel(Level.ALL);
        navigationAStar.getPathExecutor().getLog().setLevel(Level.ALL);
    }

    /////////////////////////////////////
    private static int instanceCount = 0;

    @Override
    public Initialize getInitializeCommand() {
        return new Initialize().setName("Hunter_NavigationBot-" + (++instanceCount)).setDesiredSkill(5);
    }

    ////////////////////////////////////////

    private static int agent_num = 2;

    public static void main(String args[]) throws PogamutException {
        new UT2004BotRunner(NavigationBot.class, "NavigationBot").setMain(true).setLogLevel(Level.WARNING).startAgents(agent_num);
    }
}
